Skip to content

feat(snapshots): Add snapshots list table to Releases page#112819

Merged
NicoHinderling merged 7 commits intomasterfrom
snapshots-list-ui-wip
Apr 13, 2026
Merged

feat(snapshots): Add snapshots list table to Releases page#112819
NicoHinderling merged 7 commits intomasterfrom
snapshots-list-ui-wip

Conversation

@NicoHinderling
Copy link
Copy Markdown
Contributor

@NicoHinderling NicoHinderling commented Apr 13, 2026

CleanShot 2026-04-13 at 10 24 38@2x

Adds a new Snapshots tab to the Releases page for browsing visual snapshot builds. The tab is gated behind the organizations:preprod-snapshots feature flag, independently from the existing Mobile Builds tab (preprod-frontend-routes).

What this adds:

  • PreprodBuildsSnapshotTable component with a consolidated column layout inspired by the old Emerge Tools UI:

    • Snapshot: app name + image count subtitle
    • Changes: shows lifecycle state tags (Base/Pending/Processing/Failed) when comparison isn't complete, or full diff summary text (e.g., "10 modified, 1 unchanged") when it is
    • Branch: git ref + commit SHA
    • Approval: only shows Approved/Needs Approval badges for successful comparisons
    • Created: relative timestamp
  • Backend additions to SnapshotInfo API response:

    • comparison_error_message field for rich Failed state tooltips
    • Literal types for comparison_state and approval_status for type safety on both sides
  • Fixed N+1 query issue in to_snapshot_info() by adding select_related/prefetch_related for snapshot metrics, comparisons, and approvals in the builds endpoint queryset

  • Snapshots tab visibility independently gated from Mobile Builds tab, with proper fallback if a user navigates to a tab whose feature flag is off

Add a new Snapshots tab to the Releases page, gated behind the
organizations:preprod-snapshots feature flag. The table shows snapshot
builds with columns for snapshot info, comparison changes, branch,
approval status, and creation time.

Key changes:
- New PreprodBuildsSnapshotTable component with merged Changes column
  showing lifecycle state (Base/Pending/Processing/Failed) or diff
  summary (added/removed/modified/unchanged counts)
- Separate Approval column for review status on successful comparisons
- Backend: added comparison_error_message to SnapshotInfo API response
  with literal types for comparison_state and approval_status
- Fixed N+1 queries in to_snapshot_info by using prefetched relations
- Snapshots tab independently gated from Mobile Builds tab

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions bot added Scope: Frontend Automatically applied to PRs that change frontend components Scope: Backend Automatically applied to PRs that change backend components labels Apr 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

Tag requires an explicit variant prop — use muted for neutral states,
danger instead of error for failed. TabList children must be elements,
not conditional expressions — use the hidden prop instead.

Co-Authored-By: Claude <noreply@anthropic.com>
@NicoHinderling NicoHinderling marked this pull request as ready for review April 13, 2026 17:32
@NicoHinderling NicoHinderling requested review from a team as code owners April 13, 2026 17:32
@github-actions
Copy link
Copy Markdown
Contributor

Backend Test Failures

Failures on 123c6b2 in this run:

tests/sentry/preprod/api/endpoints/test_builds.py::BuildsEndpointTest::test_one_buildlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/preprod/api/endpoints/test_builds.py:73: in test_one_build
    assert response.json() == [
E   AssertionError: assert [{'app_info':...e, ...}, ...}] == [{'app_info':...e, ...}, ...}]
E     
E     At index 0 diff: {'id': '62', 'state': 3, 'app_info': {'app_id': 'com.example.app', 'name': None, 'version': None, 'build_number': None, 'date_added': '2026-04-13T17:33:46.390490+00:00', 'date_built': None, 'artifact_type': 2, 'platform': 'android', 'build_configuration': None, 'app_icon_id': None, 'apple_app_info': None, 'android_app_info': {'has_proguard_mapping': True}}, 'vcs_info': {'head_sha': None, 'base_sha': None, 'provider': None, 'head_repo_name': None, 'base_repo_name': None, 'head_ref': None, 'base_ref': None, 'pr_number': None}, 'project_id': 4557961950789664, 'project_slug': 'bar', 'distribution_info': {'is_installable': False, 'download_count': 0, 'release_notes': None, 'error_code': None, 'error_message': None}, 'size_info': None, 'posted_status_checks': None, 'base_artifact_id': None, 'base_build_info': None, 'snapshot_info': None} != {'id': <ANY>, 'state': 3, 'app_info': {'app_id': 'com.example.app', 'name': None, 'version': None, 'build_number': None, 'date_added': <ANY>, 'date_built': None, 'artifact_type': 2, 'platform': 'android', 'build_configuration': None, 'app_icon_id': None, 'apple_app_info': None, 'android_app_info': {'has_proguard_mapping': True}}, 'base_artifact_id': None, 'base_build_info': None, 'distribution_info': {'download_count': 0, 'is_installable': False, 'release_notes': None, 'error_code': None, 'error_message': None}, 'vcs_info': {'head_sha': None, 'base_sha': None, 'provider': None, 'head_repo_name': None, 'base_repo_name': None, 'head_ref': None, 'base_ref': None, 'pr_number': None}, 'project_id': <ANY>, 'posted_status_checks': None, 'project_slug': 'bar', 'size_info': None}
E     
E     Full diff:
E       [
E           {
E               'app_info': {
E                   'android_app_info': {
E                       'has_proguard_mapping': True,
E                   },
E                   'app_icon_id': None,
E                   'app_id': 'com.example.app',
E                   'apple_app_info': None,
E                   'artifact_type': 2,
E                   'build_configuration': None,
E                   'build_number': None,
E     -             'date_added': <ANY>,
E     +             'date_added': '2026-04-13T17:33:46.390490+00:00',
E                   'date_built': None,
E                   'name': None,
E                   'platform': 'android',
E                   'version': None,
E               },
E               'base_artifact_id': None,
E               'base_build_info': None,
E               'distribution_info': {
E                   'download_count': 0,
E                   'error_code': None,
E                   'error_message': None,
E                   'is_installable': False,
E                   'release_notes': None,
E               },
E     -         'id': <ANY>,
E     ?               ^^^^^
E     +         'id': '62',
E     ?               ^^^^
E               'posted_status_checks': None,
E     -         'project_id': <ANY>,
E     +         'project_id': 4557961950789664,
E               'project_slug': 'bar',
E               'size_info': None,
E     +         'snapshot_info': None,
E               'state': 3,
E               'vcs_info': {
E                   'base_ref': None,
E                   'base_repo_name': None,
... (9 more lines)

Comment thread static/app/components/preprod/preprodBuildsSnapshotTable.tsx
Show "No changes" text when comparison succeeds but all counts are
zero, instead of rendering an empty cell. Add snapshot_info field to
the builds endpoint test expectation.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread static/app/components/preprod/preprodBuildsTable.tsx Outdated
Comment thread static/app/views/releases/list/mobileBuilds.tsx
Comment thread static/app/views/releases/list/mobileBuilds.tsx
…napshots tab

Address Cursor Bugbot findings:
- Show "No snapshots found" instead of "No mobile builds found" in
  empty state, with correct docs URL for snapshot display
- Skip mobile onboarding panel when on snapshots tab
- Use distinct analytics pageSource for snapshots tab

Co-Authored-By: Claude <noreply@anthropic.com>

const shouldShowMobileBuildsTab =
hasPreprodFeature && (hasBuildsData || hasAnyStrictlyMobileProject);
const shouldShowSnapshotsTab = !!hasSnapshotsFeature;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshots tab visible but API requires different feature flag

High Severity

shouldShowSnapshotsTab is set to !!hasSnapshotsFeature (checking only preprod-snapshots), but the BuildsEndpoint that MobileBuilds calls requires preprod-frontend-routes and returns 403 without it. If an organization has preprod-snapshots enabled but not preprod-frontend-routes, the Snapshots tab will render but every API request will fail, showing a broken error state.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b770732. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#110583 working on it :)

}, [selection.projects]);

const hasPreprodFeature = organization.features?.includes('preprod-frontend-routes');
const hasSnapshotsFeature = organization.features?.includes('preprod-snapshots');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probe query counts snapshot builds as mobile builds

Low Severity

The buildsProbeQuery doesn't include a display filter, so the API returns all builds including snapshot-only builds. This causes hasBuildsData to be true and shouldShowMobileBuildsTab to appear even for organizations that only have snapshot builds and no actual mobile builds, leading to an empty Mobile Builds tab.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b770732. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think that's fine

Comment thread src/sentry/preprod/api/models/project_preprod_build_details_models.py Outdated
Comment thread src/sentry/preprod/api/models/project_preprod_build_details_models.py Outdated
Comment thread static/app/components/preprod/preprodBuildsSnapshotTable.tsx
Comment thread static/app/components/preprod/preprodBuildsTable.tsx Outdated
Comment thread static/app/views/preprod/types/buildDetailsTypes.ts Outdated
@@ -244,6 +245,7 @@ export default function ReleasesList() {
}, [selection.projects]);

const hasPreprodFeature = organization.features?.includes('preprod-frontend-routes');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably remove this since we're GA. This boolean logic below (l286 onwards) is somewhat complex, so I think we need to address some cleanup here in a follow up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah ive been going back and forth with hector about this for the past 2 weeks and as of friday convinced him we should remove it and not stress about self-hosted since I'm already working with the self-hosted contractor

Comment thread static/app/views/releases/list/index.tsx Outdated

const shouldShowMobileBuildsTab =
hasPreprodFeature && (hasBuildsData || hasAnyStrictlyMobileProject);
const shouldShowSnapshotsTab = !!hasSnapshotsFeature;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the logic here be similar to shouldShowMobileBuildsTab? I.e. const shouldShowSnapshotsTab = hasSnapshotsFeature === true && hasSnapshotsData

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtopo27 wants to always show it - ill defer to him

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rbro112 I'm a proponent of always showing for discoverability. If we only show when people have uploaded, their likelihood of them finding Snapshots is much smaller (and it's already small since it's nested under releases).

We could do something like isFrontendPlatform and try to have some sort of list of platforms that would (or would not use snapshots), but I don't think this adds a ton of cognitive load to the UX of Releases and is harmless while giving us a boost in discoverability

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no empty state currently so we most definitely want to add an empty state and likely education on how to get started as a follow up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, I can take that as a ticket 👍

organization: Organization;
selectedProjectIds: string[];
defaultDisplay?: PreprodBuildsDisplay;
hideDisplayToggle?: boolean;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly does this do? The naming doesn't make it clear what display toggling even does

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CleanShot 2026-04-13 at 11 46 15@2x

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decides whether to hide or show this. on the snapshots tab, we don't show it

Rename SnapshotInfo to SnapshotComparisonInfo and snapshot_info to
snapshot_comparison_info across backend, frontend, and tests. Rename
to_snapshot_info to to_snapshot_comparison_info with head_artifact
parameter. Normalize dash characters and update docs URL slug.
Rename shouldShowTabs to shouldShowPreprodTabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bb6e917. Configure here.

Comment thread static/app/views/releases/list/index.tsx
The styled ReleasesPageFilterBar was still keyed on
shouldShowMobileBuildsTab, so the margin was wrong when only the
snapshots tab was visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@NicoHinderling NicoHinderling merged commit 7eeecfb into master Apr 13, 2026
60 checks passed
@NicoHinderling NicoHinderling deleted the snapshots-list-ui-wip branch April 13, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants